/*
Copyright 2007 Raymond Giorgi

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

   http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */

package edu.pitt.CS2510.Nodes;

import java.util.ArrayList;

import edu.pitt.CS2510.Coordinate;
import edu.pitt.CS2510.Request;
import edu.pitt.CS2510.Simulator;
import edu.pitt.CS2510.Update;
import edu.pitt.CS2510.Grids.Grid;
import edu.pitt.CS2510.Grids.GridPiece;
import edu.pitt.CS2510.Messages.AttackMessage;
import edu.pitt.CS2510.Messages.SetTypeMessage;
import edu.pitt.CS2510.Messages.SupplyMessage;
import edu.pitt.CS2510.Messages.UpdateGridMessage;
import edu.pitt.CS2510.NodeActions.CentralNodeActions;
import edu.pitt.CS2510.NodeActions.NodeActionsInterface;

/**
 * @author Alichino
 * 
 *         The top level of the node hierarchy. This node directly or indirectly
 *         tells every node in the system what to do
 */
public class CentralNode extends Node {
	/**
	 * 
	 * @author NoOne
	 * 
	 *         The simulator needs to keep track of which nodes sent which
	 *         updates to the grid. Nodes
	 */
	private class UpdatePair {
		int id;
		ArrayList<Coordinate> coordinates;

		UpdatePair(int id) {
			this.id = id;
		}
	}

	private ArrayList<UpdatePair> updatePairs = new ArrayList<UpdatePair>();
	private final ArrayList<Integer> idleNodes = new ArrayList<Integer>();
	private ArrayList<GridPiece> enemies = new ArrayList<GridPiece>();
	private final ArrayList<GridPiece> deferredEnemies = new ArrayList<GridPiece>();
	private final ArrayList<Integer> coordinators = new ArrayList<Integer>();
	private final ArrayList<Update> gridChanges = new ArrayList<Update>();
	private final ArrayList<Update> gridHistory = new ArrayList<Update>();
	private final ArrayList<Request> deferredRequests = new ArrayList<Request>();
	private boolean initialized = false;
	private final int id = 0;
	private CentralNodeActions messageHandler = null;
	private boolean createdUpdateList = false;

	/**
	 * Give the central node access to the simulator to send and recieve
	 * messages Give it a list of enemy nodes Give it a copy of the grid
	 * 
	 * @param sim
	 * @param enemies
	 * @param grid
	 */
	public CentralNode(Simulator sim, ArrayList<GridPiece> enemies, Grid grid) {
		super(sim, grid);
		this.enemies = (ArrayList<GridPiece>) enemies.clone();
		messageHandler = new CentralNodeActions(this);
	}

	/**
	 * Put a node on the idleNode list. This function is called by the simulator
	 * during initialization and CentralNodeActions after recieving a
	 * DoneMessage (a Supervisor finished its task)
	 * 
	 * @param node
	 */
	public void addIdleNode(int node) {
		idleNodes.add(new Integer(node));
	}

	/**
	 * On the first cycle, initialize the grid and send nodes to attack. On each
	 * subsequent cycle, process messages, check to see if there are are any
	 * enemies that were not attacked due to lack of nodes, check to see if a
	 * node requested reinforcements and didn't get them (lack of available
	 * nodes), and finally send changes to the grid to all Supervisors
	 */
	@Override
	public void performAction() {
		if (initialized == false) {
			initialized = true;
			for (final GridPiece piece : enemies) {
				final Coordinate co = new Coordinate(piece.getX(), piece.getY());
				if (!idleNodes.isEmpty()) {
					final int node = removeIdleNode(0);
					sendSetTypeMessage(node, WorkerNode.Supervisor);
					coordinators.add(node);
					sendAttackMessage(node, co);
				} else {
					deferredEnemies.add(piece);
				}
			}
			return;
		} else {
			createdUpdateList = false;
			processMessages();
			if (!deferredEnemies.isEmpty() && !idleNodes.isEmpty()) {
				for (final GridPiece piece : deferredEnemies) {
					final Coordinate co = new Coordinate(piece.getX(), piece
							.getY());
					if (!idleNodes.isEmpty()) {
						final int node = removeIdleNode(0);
						sendSetTypeMessage(node, WorkerNode.Supervisor);
						coordinators.add(node);
						sendAttackMessage(node, co);
					}
				}
			}
			if (!deferredRequests.isEmpty() && !idleNodes.isEmpty()) {
				final int node = getRequest();
				final ArrayList<Integer> temp = new ArrayList<Integer>();
				temp.add(idleNodes.remove(0));
				sendMessage(new SupplyMessage(node, getId(), temp));
			}
			if (!gridChanges.isEmpty()) {
				propegateUpdates();
			}
		}
	}

	/**
	 * Send a list of updates to all Supervisors. Care must be taken here to not
	 * send send the same updates to a Supervisor that updated the Central. In
	 * other words, when a node recieves an update, it will mark that GridPiece
	 * as used (or unused depending on the case). However, that Supervisor will
	 * have already updated its own grid. If it recieves an update, it will
	 * change it back, which will result in some inconsistancies.
	 */
	private void propegateUpdates() {
		final ArrayList<Update> temp = new ArrayList<Update>();
		for (final Integer node : coordinators) {
			temp.clear();
			for (final Update update : gridChanges) {
				if (node != update.getFrom()) {
					temp.add(update);
				}
			}
			sendUpdateMessage(node, temp);
		}
	}

	/**
	 * Send an update message to the node who's id is 'node.' This should only
	 * be used during the first cycle. All subsequent updates should be sent
	 * through 'propegateUpdates()'
	 * 
	 * @param node
	 * @param list
	 */
	private void sendUpdateMessage(int node, ArrayList<Update> list) {
		final ArrayList<Coordinate> coordinates = new ArrayList<Coordinate>();
		for (final Update update : list) {
			coordinates.add(update.getCoordinate());
		}
		sendMessage(new UpdateGridMessage(node, id, coordinates));
	}

	/**
	 * Tell a node whether it should be Supervisor or Grunt
	 * 
	 * @param node
	 * @param type
	 */
	private void sendSetTypeMessage(int node, int type) {
		sendMessage(new SetTypeMessage(node, id, type));
	}

	/**
	 * Add a node to the supervisor list. This is necessary so that certain
	 * messages (updates in particular) are only sent to Supervisors
	 * 
	 * @param node
	 */
	public void addSupervisor(Node node) {
		coordinators.add(new Integer(node.getId()));
	}

	/**
	 * set the list of enemies to 'enemies.' The simulator calls this when the
	 * list of enemies changes (i.e. when one is added after the Central has
	 * already been initialized)
	 * 
	 * @param enemies
	 */
	public void setEnemies(ArrayList<GridPiece> enemies) {
		this.enemies = enemies;
	}

	/**
	 * Update the grid with a list of coordinates
	 * 
	 * @param updates
	 */
	public void updateGrid(ArrayList<Coordinate> updates) {
		for (final Coordinate co : updates) {
			final int x = co.getX();
			final int y = co.getY();
			if (grid.isUsedAtBlock(x, y)) {
				grid.setUsedAtBlock(x, y, false);
			} else {
				grid.setUsedAtBlock(x, y, true);
			}
		}
	}

	/**
	 * Send an attack message to a Supervisor
	 * 
	 * @param node
	 * @param co
	 */
	private void sendAttackMessage(int node, Coordinate co) {
		sendMessage(new AttackMessage(node, id, co));
	}

	/**
	 * Returns a list of Supervisors
	 * 
	 * @return coordinators
	 */
	public ArrayList<Integer> getCoordinators() {
		return coordinators;
	}

	/**
	 * Add one update to the list of grid updates
	 * 
	 * @param update
	 */
	public void addGridChange(Update update) {
		gridChanges.add(update);
	}

	/**
	 * Return a list of nodes that are idle, no being used
	 * 
	 * @return idleNodes
	 */
	public ArrayList<Integer> getIdleNodes() {
		return idleNodes;
	}

	/**
	 * This function is used to pop an idle node off of a list. In other words
	 * it takes the first idle node, removes it from the list, and returns its
	 * id. Care must be taken to add this node to another list. Otherwise, it
	 * will be lost.
	 * 
	 * @param no
	 * @return the id of an idle node
	 */
	public int removeIdleNode(int no) {
		if (!idleNodes.isEmpty()) {
			return (idleNodes.remove(0)).intValue();
		}
		return -1;
	}

	/**
	 * This returns the CentralNode's unique id
	 * 
	 * @return id
	 */
	@Override
	public int getId() {
		return id;
	}

	/**
	 * This function goes largely unused for Central, but it returns this node's
	 * messageHandler
	 * 
	 * @return this node's messageHandler
	 */
	@Override
	public NodeActionsInterface getMessageHandler() {
		return messageHandler;
	}

	/**
	 * If a node requested backup but there weren't enough idle nodes to handle
	 * it, this function adds it to a list so that it can be taken care of later
	 * 
	 * @param from
	 * @param no
	 */
	public void addRequest(int from, int no) {
		deferredRequests.add(new Request(from, no));
	}

	/**
	 * If a node didn't get a request fulfilled due to lack of resources (no
	 * available nodes), this function is called to provide them with a node. It
	 * pops the first request off the list, provides that Supervisor with one
	 * Grunt, then adds this request to the back of the queue. This was done in
	 * the interest of fairness. A Supervisor should not be able to hog Grunts
	 * while other Supervisors need help as well.
	 * 
	 * @return the id of a node requesting backup
	 */
	public int getRequest() {
		final Request request = deferredRequests.get(0);
		request.decrement();
		if (request.getNo() == 0) {
			deferredRequests.remove(0);
		} else {
			deferredRequests.add(deferredRequests.remove(0));
		}
		return request.getFrom();
	}

	/**
	 * Every cycle in which updates are to be sent should call this method. It
	 * gets rid of old updates and only populates the list with current ones
	 */
	public void initializeUpdateList() {
		if (!createdUpdateList) {
			updatePairs.clear();
			for (Integer integer : coordinators) {
				updatePairs.add(new UpdatePair(integer));
			}
		}
	}
}
